Hasta ahora hemos analizado cada variable por separado utilizando diferentes métodos para detectar outliers segun las propiedades individuales de cada una.

No obstante, a veces los outliers aparecen en la combinación de varias variables. Por ello, a continuación estudiaremos estos valores atípicos mediante métodos multivariante.

Para detectaros, emplearemos métodos como la distancia de Mahalanobis, así como algoritmos más avanzados de detección de anomalías basados en la densidad, tales como el Local Outlier Factor.

Distancia de Mahalanobis

Calculamos la distancia de Mahalanobis para identificar observaciones alejadas del centro multivariante de los datos (medias de cada variable). Mediante un histograma y gráfico de densidad visualizaremos la distribución de dichas distancias y detectaremos posibles outliers.

load("~/Documents/2025-2026/mineria de datos/data.RData")
dades <- data[, varNum]
for(v in varNum){
  dades[[v]] <- as.numeric(as.character(dades[[v]]))
}

dades2 <- na.omit(dades)

distancia_mahalanobis <- mahalanobis(
  dades2,
  center = colMeans(dades2),
  cov    = cov(dades2)
)
#Graficamos el plot de la densidad de las distancias
plot(density(distancia_mahalanobis))

Se observa que la gran mayoría de los clientes están dentro de un rango normal, no obstante, existe un grupo pequeño con distancias muy elevadas que se pueden considerar claramente outliers.

La cola larga a la derecha es la señal principal de anomalías.

A continuación se muestran aquellos individuos de la bbdd que quedan sobre del 99% de la distribución \(\chi^2\)

cutoff <- qchisq(p = 0.99, df = ncol(dades2))
dades2[distancia_mahalanobis>cutoff, ]
##      Age CreditScore Tenure EstimatedSalary   Balance NumOfProducts
## 1030  32         593      9        53931.05 134096.53             2
## 3226  84         537      8       186235.98  92242.34             1
## 3321  49         819      1       166164.30 120656.86             4
## 8938  73         735      9       114283.33      0.00             1
##      TransactionFrequency AvgTransactionAmount DigitalEngagementScore
## 1030                   35            108.14348                     87
## 3226                   27             72.55073                     61
## 3321                   32             82.13647                     31
## 8938                   27             98.62025                     81
##      ComplaintsCount NetPromoterScore
## 1030               4                0
## 3226               0               10
## 3321               0                9
## 8938               3                0

Identificamos varios valores que podrían considerarse atípicos en comparación con el resto de los datos. Por ejemplo, la edad de 84 años resulta inusualmente elevada dentro de una población bancaria típica. De manera similar, saldos muy altos como 134,096.53 o 120,656.86 contrastan marcadamente con aquellos clientes que incluso mantienen un balance de cero. Además, la frecuencia y el valor promedio de las transacciones varían de forma considerable entre los clientes, lo que sugiere la existencia de perfiles financieros extremos y diferenciados del comportamiento general.

Dentro de los registros analizados destaca el individuo 8938. A pesar de llevar 9 años como cliente y disponer de un ingreso estable coherente con un perfil de persona jubilada, su historial bancario aparece prácticamente vacío. No obstante, el número de transacciones y el importe promedio de las mismas resultan inusualmente elevados, lo que sugiere un comportamiento atípico que merece una revisión más detallada.

Una vez calculadas las distancias, y ordenadas en orden descendente, identificamos fácilmente aquellas con valores más extremos.

dades2 <- dades2[order(distancia_mahalanobis, decreasing = TRUE),]
par(mfrow=c(1,1))
hist(distancia_mahalanobis, 
     main = "Histograma de distancias de Mahalanobis", 
     xlab = "Distancia de Mahalanobis",
     col = "lightblue", 
     breaks = 30)
abline(v = cutoff, col = "red", lwd = 2)

El histograma de la distancia de Mahalanobis muestra que la mayoría de las observaciones tienen valores bajos (entre 5 y 15), mientras que en la cola de la derecha, unas pocas presentan distancias mucho más altas (superiores a 25). En el gráfico se indica el punto que corresponde al 99 % de la distribución \(\chi^2\).

A continuación, para estudiar la relación entre variables hacemos un utilizamos un gráfico scatterplot 3D para observar los outliers multivariantes. Para ello elegimos las variables: Age, Balance, EstimatedSalary.

El gráfico 3D muestra la relación entre edad, balance y salario estimado, donde los puntos negros representan los valores normales y los rojos indican outliers detectados mediante la distancia de Mahalanobis; estas anomalías corresponden a individuos con combinaciones atípicas de variables que se alejan del patrón general de la muestra.

library(scatterplot3d)
library(plotly)
## Loading required package: ggplot2
## 
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout
umbral <- cutoff

# Crear columna de outlier
dades2$outlier <- (distancia_mahalanobis > umbral)

# Colores para outliers vs normales
dades2$color <- ifelse(dades2$outlier, "red", "black")

scatterplot3d(dades2[, "Age"], 
              dades2[, "Balance"], 
              dades2[, "EstimatedSalary"],
              color = dades2$color,
              pch = 19,
              main = "Outliers según distancia de Mahalanobis")

fig <- plot_ly(dades2, 
               x = ~Age, 
               y = ~Balance, 
               z = ~EstimatedSalary,
               color = ~outlier,
               colors = c("black", "red"),
               marker = list(size = 4)) %>%
       add_markers() %>%
       layout(title = "Outliers multivariantes (Mahalanobis)")
fig
(who <- which(dades2[, "outlier"] == TRUE))
## [1]  27  58  59 177

Local outlier Factor

El método Local Outlier Factor (LOF) evalúa si un punto de los datos es un valor atípico comparando su densidad local con la de sus vecinos más cercanos. La idea central consiste en que, si un punto presenta una densidad significativamente menor que la de su entorno, se considera una anomalía o outlier.

library(Rlof)
## Loading required package: doParallel
## Loading required package: foreach
## Loading required package: iterators
## Loading required package: parallel
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(rgl)  # para gráficos 3D

# Selección de tres variables numéricas
dades_sel <- dades2[, c("Age", "Balance", "EstimatedSalary")]

# Calcular LOF usando Rlof
outlier.scores <- Rlof::lof(dades_sel, k = 5)

# Ver distribución de los scores
plot(density(outlier.scores), main = "Distribución LOF (k=5)")

# Identificar los 5 principales outliers
outliers <- order(outlier.scores, decreasing = TRUE)[1:5]

# Etiquetas para un biplot PCA
n <- nrow(dades_sel)
labels <- rep(".", n)
labels[outliers] <- "+"
biplot(prcomp(dades_sel), cex = .8, xlabs = labels)
## Warning in arrows(0, 0, y[, 1L] * 0.8, y[, 2L] * 0.8, col = col[2L], length =
## arrow.len): zero-length arrow is of indeterminate angle and so skipped

# Marcar outliers en pares de variables
pch <- rep(".", n)
pch[outliers] <- "+"
col <- rep("black", n)
col[outliers] <- "red"
pairs(dades_sel, pch = pch, col = col, main = "Outliers detectados por LOF")

# Gráfico 3D
plot3d(dades_sel[,1], dades_sel[,2], dades_sel[,3],
       type = "s", col = col, size = 1,
       xlab = "Age", ylab = "Balance", zlab = "EstimatedSalary")

Se detectan clientes con comportamientos inusuales, como alguien muy joven con balance muy alto, personas con salarios estimados extremadamente diferentes al resto, o combinaciones de características que no siguen el patrón típico de la mayoría de observaciones.